<template>
  <div class="g6-board">
    <div class="g6-board--body" ref="canvas" @contextmenu.prevent></div>

    <div class="g6-board--zoom-tip" v-show="zoom.visible">{{ zoom.value }}</div>

    <fields-dialog :fields="options.nodeFields" :title="dialogs.node.editItem ? '编辑节点' : '添加节点'"
                   :visible.sync="dialogs.node.visible" v-model="dialogs.node.form" :graph="graph" @ok="onNodeOk"/>

    <fields-dialog :fields="options.edgeFields" title="编辑连接" :visible.sync="dialogs.edge.visible"
                   v-model="dialogs.edge.form" :graph="graph" @ok="onEdgeOk"/>

    <fields-dialog :fields="options.comboFields" :title="dialogs.combo.editItem ? '编辑分组' : '添加分组'"
                   :visible.sync="dialogs.combo.visible" v-model="dialogs.combo.form" :graph="graph" @ok="onComboOk"/>

    <contextmenu ref="contextmenu" :title="contextmenuTitle"></contextmenu>
  </div>
</template>

<script>
import ResizeObserver from 'resize-observer-polyfill'
import FieldsDialog from './fieldsdialog.vue'
import contextmenu from './contextmenu'
import EventBus from './metas/events'
import './setup'
import anchorPoints from './metas/anchorPoints'

const getG6 = async () => {
  if (typeof window !== 'undefined' && typeof window.G6 !== 'undefined') {
    return window.G6
  }
  const lib = await import('@antv/g6')
  return lib.default
}

export default {
  components: {FieldsDialog, contextmenu},
  props: {
    title: {
      type: String,
      default: '图编辑器'
    },
    data: {
      type: Object,
      required: true
    },
    editMode: {
      type: Boolean,
      default: false
    },
    /**
     * @type {BoardOptions}
     */
    options: {
      type: Object,
      required: true
    }
  },
  watch: {
    editMode() {
      if (!this.graph) {
        return
      }
      this.updateMode()
    }
  },
  data() {
    return {
      resizeObserver: null,
      resizeTimer: null,
      /**
       * @type {G6.Graph}
       */
      graph: null,
      dialogs: {
        node: {
          editItem: null,
          visible: false,
          form: {},
          position: {
            x: 0,
            y: 0
          }
        },
        edge: {
          visible: false,
          form: {}
        },
        combo: {
          editItem: null,
          visible: false,
          form: {},
          position: {
            x: 0,
            y: 0
          }
        }
      },
      contextmenuTitle: '操作',
      contextmenus: {
        canvas: [{
          label: '添加节点',
          command: 'add-node',
          handler: (e) => {
            this.dialogs.node.editItem = null
            this.dialogs.node.form = {}
            this.dialogs.node.position.x = e.x
            this.dialogs.node.position.y = e.y

            this.dialogs.node.visible = true
          }
        }, {
          label: '添加分组',
          command: 'add-combo',
          handler: (e) => {
            this.dialogs.combo.editItem = null
            this.dialogs.combo.form = {}
            this.dialogs.combo.position.x = e.x
            this.dialogs.combo.position.y = e.y

            this.dialogs.combo.visible = true
          }
        }],
        node: [{
          label: '编辑节点',
          command: 'edit-node',
          handler: (e) => {
            const item = e.item
            const data = item.getModel()

            const e1 = {
              type: 'node',
              data: data,
              item: item,
              graph: this.graph
            }

            let form

            const handler = this.options.on.edit.before
            if (handler) {
              // 如果有返回值，则使用返回值作为节点数据
              const handleResult = handler(e1)
              // 返回 false 取消编辑
              if (handleResult === false) {
                return
              }
              if (handleResult === undefined) {
                form = {
                  ...data
                }
              } else {
                form = handleResult
              }
            } else {
              form = {
                ...data
              }
            }

            this.dialogs.node.form = form
            this.dialogs.node.editItem = item
            this.dialogs.node.visible = true
          }
        }, {
          label: '移除节点',
          command: 'remove-node',
          handler: (e) => {
            this.$confirm('您正在移除节点，此操作会同时移除与此节点相连接的边，是否继续？', '提示').then(() => {
              const item = e.item
              const data = item.getModel()
              this.graph.removeItem(item)
              this.emitChangeEvent('node-remove', {
                item: item,
                data: data
              })
            }).catch(() => {
            })
          }
        }],
        edge: [{
          label: '编辑连接',
          command: 'edit-edge',
          handler: (e) => {
            const item = e.item
            const data = item.getModel()

            const e1 = {
              type: 'edge',
              data: data,
              item: item,
              graph: this.graph
            }

            let form

            const handler = this.options.on.edit.before
            if (handler) {
              // 如果有返回值，则使用返回值作为节点数据
              const handleResult = handler(e1)
              // 返回 false 取消编辑
              if (handleResult === false) {
                return
              }
              if (handleResult === undefined) {
                form = {
                  ...data
                }
              } else {
                form = handleResult
              }
            } else {
              form = {
                ...data
              }
            }

            this.dialogs.edge.form = form
            this.dialogs.edge.editItem = item
            this.dialogs.edge.visible = true
          }
        }, {
          label: '移除连接',
          command: 'remove-edge',
          handler: (e) => {
            this.$confirm('您正在移除连接，是否继续？', '提示').then(() => {
              const item = e.item
              const data = item.getModel()
              this.graph.removeItem(item)
              this.emitChangeEvent('edge-remove', {
                item: item,
                data: data
              })
            }).catch(() => {
            })
          }
        }],
        combo: [{
          label: '添加节点',
          command: 'add-node',
          handler: (e) => {
            const comboId = e.item.getID()
            this.dialogs.node.editItem = null
            this.dialogs.node.form = {
              comboId
            }
            this.dialogs.node.position.x = e.x
            this.dialogs.node.position.y = e.y

            this.dialogs.node.visible = true
          }
        }, {
          label: '添加分组',
          command: 'add-combo',
          handler: (e) => {
            const comboId = e.item.getID()
            this.dialogs.combo.editItem = null
            this.dialogs.combo.form = {
              parentId: comboId
            }
            this.dialogs.combo.position.x = e.x
            this.dialogs.combo.position.y = e.y

            this.dialogs.combo.visible = true
          }
        }, {}, {
          label: '编辑分组',
          command: 'edit-combo',
          handler: (e) => {
            const item = e.item
            const data = item.getModel()

            const e1 = {
              type: 'combo',
              data: data,
              item: item,
              graph: this.graph
            }

            let form

            const handler = this.options.on.edit.before
            if (handler) {
              // 如果有返回值，则使用返回值作为节点数据
              const handleResult = handler(e1)
              // 返回 false 取消编辑
              if (handleResult === false) {
                return
              }
              if (handleResult !== undefined) {
                form = handleResult
              }
            } else {
              form = {
                ...data
              }
            }

            this.dialogs.combo.form = form
            this.dialogs.combo.editItem = item

            this.dialogs.combo.visible = true
          }
        }, {
          label: '解散分组',
          command: 'remove-combo',
          handler: (e) => {
            this.$confirm('您正在解散分组，是否继续？', '提示').then(() => {
              const item = e.item
              const data = item.getModel()
              this.graph.uncombo(item)
              this.emitChangeEvent('combo-remove', {
                item: item,
                data: data
              })
            }).catch(() => {
            })
          }
        }]
      },
      zoom: {
        handleTimer: -1,
        visibleTimer: -1,
        value: 100,
        visible: false
      },
      tooltipPlugins: [],
      gridPlugin: null,
      snapLinePlugin: null
    }
  },
  mounted() {
    this.init()
    this.watchSize()
  },
  beforeDestroy() {
    if (this.resizeObserver && this.$el) {
      this.resizeObserver.unobserve(this.$el)
    }
    clearTimeout(this.resizeTimer)
    this.unbindMethods()
  },
  methods: {
    watchSize: function watchSize() {
      const ro = new ResizeObserver((entries) => {
        // 只需要第一个元素
        const item = entries[0]
        const rect = item.contentRect
        const width = rect.width
        const height = rect.height
        this.doResize(width, height)
      })
      this.resizeObserver = ro
      ro.observe(this.$el)
    },
    doResize: function doResize(width, height) {
      clearTimeout(this.resizeTimer)
      this.resizeTimer = setTimeout(() => {
        this.graph.changeSize(width, height)
      }, 500)
    },
    async init() {
      const styles = this.options.styles
      const size = this.getBounds()
      // const tc = document.createElement('div');
      // tc.id = 'toolbarContainer';
      // tc.style.position = 'absolute'
      // tc.style.top = '60px'
      // tc.style.right = '20px'
      // tc.style.width = '400px'
      // document.body.appendChild(tc);
      // const toolbar = new G6.ToolBar({
      //   container: tc,
      //   getContent: () => {
      //     return `
      //       <ul>
      //         <li code='add'>增加节点</li>
      //         <li code='undo'>撤销</li>
      //       </ul>
      //     `
      //   },
      //   handleClick: (code, graph) => {
      //     if (code === 'add') {
      //       graph.addItem('node', {
      //         id: 'node2',
      //         label: 'node2',
      //         x: 300,
      //         y: 150
      //       })
      //     } else if (code === 'undo') {
      //       toolbar.undo()
      //     }
      //   }
      // })

      const G6 = await getG6()

      const plugins = []
      if (this.options.tooltip.node) {
        // 允许出现 tooltip 的 item 类型
        this.options.tooltip.node.itemTypes = ['node']
        const tooltipOption = this.options.tooltip.node
        tooltipOption.shouldBegin = tooltipOption.shouldBegin.bind(this)
        tooltipOption.getContent = tooltipOption.getContent.bind(this)
        this.tooltipPlugins.push(new G6.Tooltip(this.options.tooltip.node))
      }
      if (this.options.tooltip.edge) {
        // 允许出现 tooltip 的 item 类型
        this.options.tooltip.edge.itemTypes = ['edge']
        const tooltipOption = this.options.tooltip.edge
        tooltipOption.shouldBegin = tooltipOption.shouldBegin.bind(this)
        tooltipOption.getContent = tooltipOption.getContent.bind(this)
        this.tooltipPlugins.push(new G6.Tooltip(this.options.tooltip.edge))
      }

      plugins.push(...this.tooltipPlugins)

      // 创建 G6 图实例
      const graph = this.graph = new G6.Graph({
        // 指定图画布的容器 id
        container: this.$refs.canvas,
        enabledStack: true,
        // 必须将 groupByTypes 设置为 false，带有 combo 的图中元素的视觉层级才能合理
        groupByTypes: false,
        plugins,
        modes: {
          // 支持的 behavior
          default: [
            'zoom-canvas',
            'click-select',
            // 'activate-relations',
            'select-item',
            'drag-canvas'
          ],
          edit: [
            'click-select',
            'select-item',
            // 'zoom-canvas',
            'drag-node',
            'drag-combo',
            // 'create-edge',
            'add-edge',
            'contextmenu',
            'drag-canvas',
            {
              type: 'brush-select',
              brushStyle: {
                fill: '#00000088',
                lineWidth: 2
              },
              trigger: 'shift'
            }
          ]
        },
        // 画布宽高
        width: size.width,
        height: size.height,
        defaultNode: {
          anchorPoints: anchorPoints.getPoints(),
          ...styles.node
        },
        defaultEdge: {
          style: {
            lineAppendWidth: 5
          },
          ...styles.edge
        },
        defaultCombo: {
          type: 'rect-ext',
          // padding: 0,
          // style: {

          // }
          labelCfg: {
            style: {
              fontSize: 24
            }
          },
          anchorPoints: anchorPoints.getPoints(),
          ...styles.combo
        },
        nodeStateStyles: {
          ...styles.nodeStates
        },
        edgeStateStyles: {
          ...styles.edgeStates
        }
      })
      graph.data(this.data)
      graph.render()

      graph.$opts = this.options

      window.g6board = graph

      // // 处理多边场景
      // const offsetDiff = 10;
      // const multiEdgeType = 'quadratic';
      // const singleEdgeType = 'polyline';
      // const loopEdgeType = 'loop';
      // G6.Util.processParallelEdges(this.data.edges, offsetDiff, multiEdgeType, singleEdgeType, loopEdgeType);

      this.bindMethods()

      this.updateMode().then(() => {
        this.$emit('ready', {
          graph
        })
      })
    },
    async updateMode() {
      const mode = this.editMode ? 'edit' : 'default'
      this.graph.setMode(mode)

      if (!this.editMode) {
        this.$refs.contextmenu.hide()
      }

      const G6 = await getG6()

      // 网格插件
      if (this.editMode) {
        this.gridPlugin = new G6.Grid()
        this.graph.addPlugin(this.gridPlugin)
        this.snapLinePlugin = new G6.SnapLine()
        this.graph.addPlugin(this.snapLinePlugin)
      } else {
        if (this.gridPlugin) {
          this.graph.removePlugin(this.gridPlugin)
        }
        if (this.snapLinePlugin) {
          this.graph.removePlugin(this.snapLinePlugin)
        }
      }

      // tooltip 插件，在编辑模式下禁用
      const tooltipEnabled = !this.editMode
      this.tooltipPlugins.forEach(tooltip => {
        tooltip.set('enabled', tooltipEnabled)
      })

      // 更新连线的箭头：
      // 在编辑模式时展示，预览模式时隐藏
      const showEndArrow = this.editMode
      this.data.edges.forEach(edge => {
        if (!edge.style) {
          edge.style = {}
        }
        edge.style.endArrow = showEndArrow

        // 编辑和预览时设置指定边隐藏
        edge.visible = this.editMode || (edge.style.visible !== false)
      })
      this.graph.render()
    },
    getBounds() {
      const rect = this.$refs.canvas.getClientRects()[0]
      return {
        width: rect.width,
        height: rect.height,
        left: rect.left,
        top: rect.top
      }
    },
    bindMethods() {
      EventBus.on('canvas:contextmenu', this.onCanvasContextMenu)
      EventBus.on('node:contextmenu', this.onNodeContextMenu)
      EventBus.on('edge:contextmenu', this.onEdgeContextMenu)
      EventBus.on('combo:contextmenu', this.onComboContextMenu)
      EventBus.on('edge:before-add', this.onBeforeAddEdge)
      EventBus.on('node:after-draw', this.onAfterDrawNode)
      EventBus.on('node:after-update', this.onAfterUpdateNode)
      this.graph.on('wheelzoom', this.onCanvasZoom)
      this.graph.on('custom:add-edge', this.onEdgeAdded)
      this.graph.on('node:dragend', this.onNodeDragEnd)
      this.graph.on('combo:dragend', this.onComboDragEnd)
    },
    unbindMethods() {
      EventBus.off('canvas:contextmenu', this.onCanvasContextMenu)
      EventBus.off('node:contextmenu', this.onNodeContextMenu)
      EventBus.off('edge:contextmenu', this.onEdgeContextMenu)
      EventBus.off('combo:contextmenu', this.onComboContextMenu)
      EventBus.off('edge:before-add', this.onBeforeAddEdge)
      EventBus.off('node:after-draw', this.onAfterDrawNode)
      EventBus.off('node:after-update', this.onAfterUpdateNode)
      this.graph.off('wheelzoom', this.onCanvasZoom)
      this.graph.off('custom:add-edge', this.onEdgeAdded)
      this.graph.off('node:dragend', this.onNodeDragEnd)
      this.graph.off('combo:dragend', this.onComboDragEnd)
    },
    onAfterDrawNode(e) {
      const handler = this.options.on.node.draw
      if (!handler) {
        return
      }
      handler({
        type: 'draw',
        ...e
      })
    },
    onAfterUpdateNode(e) {
      const handler = this.options.on.node.update
      if (!handler) {
        return
      }

      handler({
        type: 'update',
        ...e
      })
    },
    onBeforeAddEdge(e) {
      const handler = this.options.on.edge.beforeAdd
      if (!handler) {
        return
      }
      const result = handler({
        graph: this.graph,
        data: e.option
      })
      if (result === false) {
        e.cancel()
      }
    },
    onEdgeAdded(e) {
      this.contextmenus.edge[0].handler(e)
      this.emitChangeEvent('edge-add', {
        item: e.item,
        data: e.item.getModel()
      })
    },
    onNodeDragEnd(e) {
      this.emitChangeEvent('node-drag', {
        item: e.item,
        data: e.item.getModel()
      })
    },
    onComboDragEnd(e) {
      this.emitChangeEvent('combo-drag', {
        item: e.item,
        data: e.item.getModel()
      })
    },
    onCanvasZoom(e) {
      clearTimeout(this.zoom.handleTimer)
      clearTimeout(this.zoom.visibleTimer)

      this.zoom.handleTimer = requestAnimationFrame(() => {
        const zoom = this.graph.getZoom()
        this.zoom.value = (Math.round(zoom * 100))
        this.zoom.visible = true

        this.$emit('zoom', {
          type: 'zoom',
          value: zoom,
          event: e,
          graph: this.graph
        })

        this.zoom.visibleTimer = setTimeout(() => {
          this.zoom.visible = false
        }, 1000)
      })
    },
    onCanvasContextMenu(e) {
      this.contextmenuTitle = '操作'
      this.clearSelectedNode()
      this.showContextMenu(e, 'canvas')
    },
    onNodeContextMenu(e) {
      this.contextmenuTitle = '节点操作'
      this.clearSelectedNode()
      this.graph.setItemState(e.item, 'selected', true)
      this.showContextMenu(e, 'node')
    },
    onComboContextMenu(e) {
      this.contextmenuTitle = '分组操作'
      this.graph.setItemState(e.item, 'selected', true)
      this.showContextMenu(e, 'combo')
    },
    onEdgeContextMenu(e) {
      this.contextmenuTitle = '边操作'
      this.clearSelectedNode()
      this.showContextMenu(e, 'edge')
    },
    showContextMenu(e, type) {
      const item = e.item

      // 检查是否允许打开
      const beforeShow = this.options.on.contextmenu.beforeShow
      let items = [...this.contextmenus[type]]
      if (beforeShow) {
        const e = {
          type,
          item,
          data: item?.getModel(),
          graph: this.graph,
          items
        }
        if (beforeShow.call(this.graph, e) === false) {
          this.$refs.contextmenu.hide()
          return
        }
        items = e.items
      }
      if (!items || !items.length) {
        // 没有菜单项，不显示右键菜单
        return
      }
      this.$refs.contextmenu.show(e, type, item, items)
    },
    async onNodeOk() {
      const editItem = this.dialogs.node.editItem
      let form = this.dialogs.node.form

      // 添加时，要设置节点的位置
      if (!editItem) {
        form.x = this.dialogs.node.position.x
        form.y = this.dialogs.node.position.y
      }
      const e = {
        type: 'node',
        data: form,
        item: editItem,
        graph: this.graph
      }

      const editHandler = this.options.on.edit.after
      if (editHandler) {
        // 如果有返回值，则使用返回值作为节点数据
        let handleResult
        try {
          handleResult = await editHandler(e)
        } catch (e) {
          this.$message.warning(e.message)
          return
        }
        // 返回 false 取消编辑
        if (handleResult === false) {
          return
        }
        if (handleResult !== undefined) {
          form = handleResult
        }
      }

      if (editItem) {
        const oldData = editItem.getModel()
        // 检查数据是否重复
        if (oldData.id !== form.id && this.data.nodes.some(node => node.id === form.id)) {
          this.$message.warning('编辑失败，此数据已经存在')
          return
        }

        this.graph.updateItem(editItem, form)
        this.emitChangeEvent('node-update', {
          item: editItem,
          data: form
        })
      } else {
        // 检查数据是否重复
        if (this.data.nodes.some(node => node.id === form.id)) {
          this.$message.warning('添加失败，此数据已经存在')
          return
        }
        const newItem = this.graph.addItem('node', form)
        newItem.toFront()
        this.emitChangeEvent('node-add', {
          item: null,
          data: form
        })
      }
      this.dialogs.node.visible = false
    },
    emitChangeEvent(type, e) {
      if (!this.editMode) {
        // 非编辑模式下，不触发变更事件
        return
      }
      this.$nextTick(() => {
        e.type = type
        e.targetType = type.split('-')[0]
        e.graph = this.graph
        this.$emit(type, e)
        this.$emit('change', e)
      })
    },
    /**
     * 编辑边完成的函数
     */
    async onEdgeOk() {
      const editItem = this.dialogs.edge.editItem
      let form = this.dialogs.edge.form

      const e = {
        type: 'edge',
        data: form,
        item: editItem,
        graph: this.graph
      }

      const editHandler = this.options.on.edit.after
      if (editHandler) {
        // 如果有返回值，则使用返回值作为节点数据
        let handleResult
        try {
          handleResult = await editHandler(e)
        } catch (e) {
          this.$message.warning(e.message)
          return
        }
        // 返回 false 取消编辑
        if (handleResult === false) {
          return
        }
        if (handleResult !== undefined) {
          form = handleResult
        }
      }

      this.graph.updateItem(editItem, form)
      this.emitChangeEvent('edge-update', {
        item: editItem,
        data: form
      })
      this.dialogs.edge.visible = false
    },
    async onComboOk() {
      const editItem = this.dialogs.combo.editItem
      let form = this.dialogs.combo.form
      // form.type = 'rect-ext'
      // form.type = 'rect'

      // 添加时，要设置分组的位置
      if (!editItem) {
        form.x = this.dialogs.combo.position.x
        form.y = this.dialogs.combo.position.y
      }
      const e = {
        type: 'combo',
        data: form,
        item: editItem,
        graph: this.graph
      }

      const editHandler = this.options.on?.edit?.after
      if (editHandler) {
        // 如果有返回值，则使用返回值作为节点数据
        let handleResult
        try {
          handleResult = await editHandler(e)
        } catch (e) {
          this.$message.warning(e.message)
          return
        }
        // 返回 false 取消编辑
        if (handleResult === false) {
          return
        }
        if (handleResult !== undefined) {
          form = handleResult
        }
      }

      if (editItem) {
        // const oldData = editItem.getModel()
        // // 检查数据是否重复
        // if (oldData.id !== form.id && this.data.combos.some(node => node.id === form.id)) {
        //   this.$message.warning('编辑失败，此数据已经存在')
        //   return
        // }
        this.graph.updateItem(editItem, form)
        this.emitChangeEvent('combo-update', {
          item: editItem,
          data: form
        })
      } else {
        // // 检查数据是否重复
        // if (this.data.combos.some(combo => combo.id === form.id)) {
        //   this.$message.warning('添加失败，此数据已经存在')
        //   return
        // }
        const item = this.graph.addItem('combo', form)
        this.emitChangeEvent('combo-add', {
          item: item,
          data: form
        })
      }
      this.dialogs.combo.visible = false
    },
    clearSelectedNode() {
      const nodes = this.graph.findAllByState('node', 'selected')
      nodes.forEach(node => {
        this.graph.clearItemStates(node, 'selected')
      })
    },
    highlightNode(node) {
      this.graph.setItemState(node, 'highlight', true)
    },
    clearHighlightNodes() {
      const nodes = this.graph.getNodes()
      nodes.forEach(node => {
        this.graph.clearItemStates(node, 'highlight')
      })
    },
    // ---------- 以下是公开接口 --------------
    getData() {
      return this.graph.save()
    },
    getGraph() {
      return this.graph
    },
    undo() {
      this.graph.undo()
    },
    redo() {
      this.graph.redo()
    },
    clearSelection() {
      this.clearHighlightNodes()
    },
    findNode(predicator, highlight) {
      if (highlight) {
        this.clearHighlightNodes()
      }
      return this.graph.findAll('node', node => {
        const data = node.getModel()
        const hit = predicator(data)
        if (hit && highlight) {
          this.highlightNode(node)
        }
        return hit
      })
    },
    /**
     * 将图导出为图片
     * @param {String} [name=graph] 图片的名称
     * @param {'image/png'|'image/jpeg'|'image/webp'|'image/bmp'} [type] 图片的类型。图的 renderer 为默认的 'canvas' 时生效，图的 renderer 为 'svg' 时将导出 svg 文件
     * @param {Object} [imageConfig] 图片的配置项，可选，具体字段见下方
     * @param {String} [imageConfig.backgroundColor] 图片的背景色，可选，不传值时将导出透明背景的图片
     * @param {Number|Number[]} [imageConfig.padding] 导出图片的上左下右 padding 值。当 padding 为 number 类型时，四周 padding 相等
     * @param {Number} [pixelRatio=window.devicePixelRatio] 控制导出图片的清晰度。默认使用 window.devicePixelRatio
     */
    exportImage(name, type, imageConfig, pixelRatio) {
      // 处理导出清晰度
      // 参考 https://github.com/antvis/G6/issues/2979
      let oldRatio
      if (pixelRatio) {
        oldRatio = window.devicePixelRatio
        window.devicePixelRatio = pixelRatio
      }
      this.graph.downloadFullImage(name, type, imageConfig)
      if (pixelRatio) {
        setTimeout(() => {
          window.devicePixelRatio = oldRatio
        }, 100)
      }
    },
    /**
     * 将画布上元素生成为图片的 URL
     * @param {'image/png'|'image/jpeg'|'image/webp'|'image/bmp'} [type] 图片的类型。图的 renderer 为默认的 'canvas' 时生效，图的 renderer 为 'svg' 时将导出 svg 文件
     * @param {Object} [imageConfig] 图片的配置项，可选，具体字段见下方
     * @param {String} [imageConfig.backgroundColor] 图片的背景色，可选，不传值时将导出透明背景的图片
     * @param {Number|Number[]} [imageConfig.padding] 导出图片的上左下右 padding 值。当 padding 为 number 类型时，四周 padding 相等
     * @param {Number} [pixelRatio=window.devicePixelRatio] 控制导出图片的清晰度。默认使用 window.devicePixelRatio
     */
    async exportImageURL(type, imageConfig, pixelRatio) {
      return new Promise(resolve => {
        // 处理导出清晰度
        // 参考 https://github.com/antvis/G6/issues/2979
        let oldRatio
        if (pixelRatio) {
          oldRatio = window.devicePixelRatio
          window.devicePixelRatio = pixelRatio
        }
        this.graph.toFullDataURL((response) => {
          if (pixelRatio) {
            setTimeout(() => {
              window.devicePixelRatio = oldRatio
            }, 100)
          }
          resolve(response)
        }, type, imageConfig)
      })
    }
  }
}
</script>
./setup
